﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using log4net;
using Microsoft.CSharp;
using Microsoft.SharePoint;
using SharePoint.Orm.Attributes;
using SharePoint.Orm.Helpers;

// ReSharper disable BitwiseOperatorOnEnumWihtoutFlags

namespace SharePoint.Orm.ItemProxy {
	public class SPListItemProxyGenerator {
		private static readonly ILog _log = LogManager.GetLogger(typeof(SPListItemProxyGenerator));
		private static readonly Dictionary<Type, Type> _proxyCache = new Dictionary<Type, Type>();

		public static T Create<T>(SPDataContext ctx, SPListItem listItem) {
			Type proxyType;
			if (!_proxyCache.TryGetValue(typeof(T), out proxyType)) {
				proxyType = CreateSPListItemProxy<T>();
				_proxyCache[typeof(T)] = proxyType;
			}
			return (T)Activator.CreateInstance(proxyType, ctx, listItem);
		}

		private static Type CreateSPListItemProxy<T>() {
			bool baseTypeHasSpListItemAwareInterface = typeof(ISPListItemAware).IsAssignableFrom(typeof(T));

			string typeName = typeof(T).Name + "Proxy"; // ContentTypeProxy
			CodeCompileUnit assembly = new CodeCompileUnit();
			CodeNamespace myNamespace = new CodeNamespace("SPListItemProxyGeneratorProxy");
			CodeTypeDeclaration myClass = new CodeTypeDeclaration {
				Name = typeName,
				IsClass = true,
				Attributes = MemberAttributes.Public
			};
			myClass.BaseTypes.Add(new CodeTypeReference(typeof(T))); // ContentTypeProxy : ContentType
			myClass.BaseTypes.Add(new CodeTypeReference(typeof(ISPListItemAware))); // ContentTypeProxy : ContentType,ISPListItemAware
			myClass.Members.Add(CreateContextField());
			myClass.Members.Add(CreateSPListItemField());
			myClass.Members.Add(CreateConstructor());
			myClass.Members.Add(CreateISPListItemAwareListItemMember(baseTypeHasSpListItemAwareInterface));
			foreach (OrmIdField idField in GetOrmIdFields<T>()) {
				myClass.Members.Add(CreateIdField(idField));
			}
			foreach (OrmField field in GetOrmFields<T>()) {
				myClass.Members.Add(CreatePropertyField(field));
			}
			if (IsIDocumentLibrary(typeof(T))) {
				myClass.Members.Add(CreateOpenBinaryStream());
			}
			myNamespace.Types.Add(myClass);
			assembly.Namespaces.Add(myNamespace);

			CompilerParameters compilerParameters = new CompilerParameters(
				new[] { 
					typeof(string).Assembly.Location,
					typeof(SPListItem).Assembly.Location,
					typeof(SPListItemHelper).Assembly.Location
				}) {
					GenerateInMemory = true
				};
			HashSet<string> referencedAssemblies = new HashSet<string>(BaseTypesLocations(typeof(T)).Concat(InterfaceTypeLocations(typeof(T))));
			compilerParameters.ReferencedAssemblies.Add(typeof(T).Assembly.Location);
			compilerParameters.ReferencedAssemblies.AddRange(referencedAssemblies.ToArray());
			CSharpCodeProvider ccp = new CSharpCodeProvider();
			CompilerResults compres = ccp.CompileAssemblyFromDom(compilerParameters, assembly);

			//foreach (var error in compres.Errors) {
			//    Console.WriteLine(error);
			//}
			if (_log.IsDebugEnabled) {
				StringWriter writer = new StringWriter();
				ccp.GenerateCodeFromType(myClass, writer, new CodeGeneratorOptions());
				_log.Debug("Generated Proxy\n" + writer);
			}

			return compres.CompiledAssembly.GetType("SPListItemProxyGeneratorProxy." + typeName);
		}

		private static IEnumerable<string> InterfaceTypeLocations(Type type) {
			var interfaces = type.GetInterfaces();
			if (interfaces.Length == 0) return Enumerable.Empty<string>();
			return interfaces
				.Select(i => i.Assembly.Location)
				.Concat(
					interfaces
						.Select(i => InterfaceTypeLocations(i))
						.Aggregate((s,l) => s.Concat(l)));
		}

		private static IEnumerable<string> BaseTypesLocations(Type type) {
			if (type.BaseType == null) return Enumerable.Empty<string>();
			return new[] { type.Assembly.Location }.Concat(BaseTypesLocations(type.BaseType));
		}

		private static bool IsIDocumentLibrary(Type type) {
			return typeof(IDocumentLibrary).IsAssignableFrom(type);
		}

		private static CodeMemberMethod CreateOpenBinaryStream() {
			CodeMemberMethod openBinaryStreamMethod = new CodeMemberMethod {
				Name = "OpenBinaryStream",
				Attributes = MemberAttributes.Public | MemberAttributes.Override,
				ReturnType = new CodeTypeReference(typeof(Stream))
			};
			CodeExpression spListItemHelpersRef = new CodeTypeReferenceExpression(typeof(SPListItemHelper));
			CodeMethodReferenceExpression openBinaryStreamHelperMethod = new CodeMethodReferenceExpression(spListItemHelpersRef, "OpenBinaryStream");
			CodeExpression contextFieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "__context");
			CodeExpression listItemFieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "__listItem");
			CodeExpression returnExpr = new CodeMethodInvokeExpression(openBinaryStreamHelperMethod, listItemFieldRef, contextFieldRef);
			openBinaryStreamMethod.Statements.Add(new CodeMethodReturnStatement(returnExpr));
			return openBinaryStreamMethod;
		}

		private static CodeMemberProperty CreateISPListItemAwareListItemMember(bool overrideProperty) {
			CodeMemberProperty proxyProperty = new CodeMemberProperty {
				Name = "ListItem",
				Attributes = MemberAttributes.Public,
				Type = new CodeTypeReference(typeof(SPListItem))
			};

			if (overrideProperty) {
				proxyProperty.Attributes |= MemberAttributes.Override;
			}

			CodeExpression returnExpr = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "__listItem");
			proxyProperty.GetStatements.Add(new CodeMethodReturnStatement(returnExpr));

			return proxyProperty;
		}

		private static CodeTypeMember CreateSPListItemField() {
			CodeMemberField field = new CodeMemberField(typeof(SPListItem), "__listItem") {
				Attributes = MemberAttributes.Private
			};
			return field;
		}

		private static CodeTypeMember CreateContextField() {
			CodeMemberField field = new CodeMemberField(typeof(SPDataContext), "__context") {
				Attributes = MemberAttributes.Private
			};
			return field;
		}

		private static CodeTypeMember CreateIdField(OrmIdField ormField) {
			CodeMemberProperty proxyProperty = new CodeMemberProperty {
				Name = ormField.PropertyInfo.Name,
				Attributes = MemberAttributes.Public | MemberAttributes.Override,
				Type = new CodeTypeReference(ormField.PropertyInfo.PropertyType)
			};

			CodeExpression listItemFieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "__listItem");
			CodeExpression returnExpr = new CodePropertyReferenceExpression(listItemFieldRef, "ID");
			proxyProperty.GetStatements.Add(new CodeMethodReturnStatement(returnExpr));

			// TODO: throw not implemented exception in set method.

			return proxyProperty;
		}

		private static CodeTypeMember CreatePropertyField(OrmField ormField) {
			CodeMemberProperty proxyProperty = new CodeMemberProperty {
				Name = ormField.PropertyInfo.Name,
				Attributes = MemberAttributes.Public | MemberAttributes.Override,
				Type = new CodeTypeReference(ormField.PropertyInfo.PropertyType)
			};

			CodeExpression spListItemHelpersRef = new CodeTypeReferenceExpression(typeof(SPListItemHelper));
			// Calling SPListItemHelper.GetItemValue
			CodeMethodReferenceExpression getSpListItemValueMethod = new CodeMethodReferenceExpression(spListItemHelpersRef, "GetItemValue", new CodeTypeReference(ormField.PropertyInfo.PropertyType));
			CodeExpression contextFieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "__context");
			CodeExpression listItemFieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "__listItem");
			CodeExpression fieldAttrId = new CodePrimitiveExpression(ormField.FieldAttribute.Id);
			CodeExpression fieldAttrStaticName = new CodePrimitiveExpression(ormField.FieldAttribute.StaticName);
			CodeExpression fieldAttrTitle = new CodePrimitiveExpression(ormField.FieldAttribute.Title);
			CodeExpression returnExpr = new CodeMethodInvokeExpression(getSpListItemValueMethod, listItemFieldRef, contextFieldRef, fieldAttrId, fieldAttrStaticName, fieldAttrTitle);
			proxyProperty.GetStatements.Add(new CodeMethodReturnStatement(returnExpr));

			spListItemHelpersRef = new CodeTypeReferenceExpression(typeof(SPListItemHelper));
			// Calling SPListItemHelper.SetItemValue
			CodeMethodReferenceExpression setSpListItemValueMethod = new CodeMethodReferenceExpression(spListItemHelpersRef, "SetItemValue", new CodeTypeReference(ormField.PropertyInfo.PropertyType));
			contextFieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "__context");
			listItemFieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "__listItem");
			fieldAttrId = new CodePrimitiveExpression(ormField.FieldAttribute.Id);
			fieldAttrStaticName = new CodePrimitiveExpression(ormField.FieldAttribute.StaticName);
			fieldAttrTitle = new CodePrimitiveExpression(ormField.FieldAttribute.Title);
			CodeExpression valueExpr = new CodeVariableReferenceExpression("value");
			CodeExpression setValueExpr = new CodeMethodInvokeExpression(setSpListItemValueMethod, listItemFieldRef, contextFieldRef, fieldAttrId, fieldAttrStaticName, fieldAttrTitle, valueExpr);
			proxyProperty.SetStatements.Add(setValueExpr);

			return proxyProperty;
		}

		private class OrmField {
			public PropertyInfo PropertyInfo { get; set; }
			public SPOrmFieldAttribute FieldAttribute { get; set; }
		}

		private static IEnumerable<OrmField> GetOrmFields<T>() {
			foreach (PropertyInfo propertyInfo in typeof(T).GetProperties()) {
				SPOrmFieldAttribute fieldAttribute = (SPOrmFieldAttribute)propertyInfo.GetCustomAttributes(typeof(SPOrmFieldAttribute), true).FirstOrDefault();
				if (fieldAttribute != null) {
					yield return new OrmField {
						PropertyInfo = propertyInfo,
						FieldAttribute = fieldAttribute
					};
				}
			}
		}

		public class OrmIdField {
			public PropertyInfo PropertyInfo { get; set; }
			public SPOrmIdFieldAttribute FieldAttribute { get; set; }
		}

		private static IEnumerable GetOrmIdFields<T>() {
			foreach (PropertyInfo propertyInfo in typeof(T).GetProperties()) {
				SPOrmIdFieldAttribute fieldAttribute = (SPOrmIdFieldAttribute)propertyInfo.GetCustomAttributes(typeof(SPOrmIdFieldAttribute), true).FirstOrDefault();
				if (fieldAttribute != null) {
					yield return new OrmIdField { PropertyInfo = propertyInfo, FieldAttribute = fieldAttribute };
				}
			}
		}

		private static CodeConstructor CreateConstructor() {
			CodeConstructor constructor = new CodeConstructor {
				Attributes = MemberAttributes.Public
			};
			constructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(SPDataContext), "context"));
			constructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(SPListItem), "listItem"));

			CodeExpression contextFieldExpr = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "__context");
			CodeExpression contextParameterExpr = new CodeVariableReferenceExpression("context");
			CodeAssignStatement assignFieldStmt = new CodeAssignStatement(contextFieldExpr, contextParameterExpr);
			constructor.Statements.Add(assignFieldStmt);

			CodeExpression listItemFieldExpr = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "__listItem");
			CodeExpression listItemParameterExpr = new CodeVariableReferenceExpression("listItem");
			assignFieldStmt = new CodeAssignStatement(listItemFieldExpr, listItemParameterExpr);
			constructor.Statements.Add(assignFieldStmt);

			return constructor;
		}
	}
}

// ReSharper restore BitwiseOperatorOnEnumWihtoutFlags
